#!/bin/sh
#
#

# PROVIDE: jail
# REQUIRE: LOGIN FILESYSTEMS
# BEFORE: securelevel
# KEYWORD: shutdown

. /etc/rc.subr

name="jail"
desc="Manage system jails"
rcvar="jail_enable"

start_cmd="jail_start"
start_postcmd="jail_warn"
stop_cmd="jail_stop"
config_cmd="jail_config"
console_cmd="jail_console"
status_cmd="jail_status"
extra_commands="config console status"
: ${jail_program:=/usr/sbin/jail}
: ${jail_consolecmd:=/usr/bin/login -f root}
: ${jail_jexec:=/usr/sbin/jexec}
: ${jail_jls:=/usr/sbin/jls}

need_dad_wait=

# extract_var jv name param num defval
#	Extract value from ${jail_$jv_$name} or ${jail_$name} and
#	set it to $param.  If not defined, $defval is used.
#	When $num is [0-9]*, ${jail_$jv_$name$num} are looked up and
#	$param is set by using +=.  $num=0 is optional (params may start at 1).
#	When $num is YN or NY, the value is interpreted as boolean.
#	When $num is @, the value is interpreted as an array separted by IFS.
extract_var()
{
	local i _jv _name _param _num _def _name1 _name2
	_jv=$1
	_name=$2
	_param=$3
	_num=$4
	_def=$5

	case $_num in
	YN)
		_name1=jail_${_jv}_${_name}
		_name2=jail_${_name}
		eval $_name1=\"\${$_name1:-\${$_name2:-$_def}}\"
		if checkyesno $_name1; then
			echo "	$_param = 1;"
		else
			echo "	$_param = 0;"
		fi
	;;
	NY)
		_name1=jail_${_jv}_${_name}
		_name2=jail_${_name}
		eval $_name1=\"\${$_name1:-\${$_name2:-$_def}}\"
		if checkyesno $_name1; then
			echo "	$_param = 0;"
		else
			echo "	$_param = 1;"
		fi
	;;
	[0-9]*)
		i=$_num
		while : ; do
			_name1=jail_${_jv}_${_name}${i}
			_name2=jail_${_name}${i}
			eval _tmpargs=\"\${$_name1:-\${$_name2:-$_def}}\"
			if [ -n "$_tmpargs" ]; then 
				echo "	$_param += \"$_tmpargs\";"
			elif [ $i != 0 ]; then
				break;
			fi
			i=$(($i + 1))
		done
	;;
	@)
		_name1=jail_${_jv}_${_name}
		_name2=jail_${_name}
		eval _tmpargs=\"\${$_name1:-\${$_name2:-$_def}}\"
		set -- $_tmpargs
		if [ $# -gt 0 ]; then
			echo -n "	$_param = "
			while [ $# -gt 1 ]; do
				echo -n "\"$1\", "
				shift
			done
			echo "\"$1\";"
		fi
	;;
	*)
		_name1=jail_${_jv}_${_name}
		_name2=jail_${_name}
		eval _tmpargs=\"\${$_name1:-\${$_name2:-$_def}}\"
		if [ -n "$_tmpargs" ]; then
			echo "	$_param = \"$_tmpargs\";"
		fi
	;;
	esac
}

# parse_options _j _jv
#	Parse options and create a temporary configuration file if necessary.
#
parse_options()
{
	local _j _jv _p
	_j=$1
	_jv=$2

	_confwarn=0
	if [ -z "$_j" ]; then
		warn "parse_options: you must specify a jail"
		return
	fi
	eval _jconf=\"\${jail_${_jv}_conf:-/etc/jail.${_j}.conf}\"
	eval _rootdir=\"\$jail_${_jv}_rootdir\"
	eval _jconfdir=\"/etc/jail.conf.d/${_j}.conf\"
	eval _hostname=\"\$jail_${_jv}_hostname\"
	if [ -z "$_rootdir" -o \
	     -z "$_hostname" ]; then
		if [ -r "$_jconf" ]; then
			_conf="$_jconf"
			return 0
		elif [ -r "$_jconfdir" ] && ! egrep -q \
		    '^\s*\.include\s*["'\'']?/etc/jail.conf.d/' "$jail_conf" \
		    2>/dev/null; then
			_conf="$_jconfdir"
			return 0
		elif [ -r "$jail_conf" ]; then
			_conf="$jail_conf"
			return 0
		else
			warn "Invalid configuration for $_j " \
			    "(no jail.conf, no hostname, or no path).  " \
			    "Jail $_j was ignored."
		fi
		return 1
	fi
	eval _ip=\"\$jail_${_jv}_ip\"
	if [ -z "$_ip" ] && ! check_kern_features vimage; then
		warn "no ipaddress specified and no vimage support.  " \
		    "Jail $_j was ignored."
		return 1
	fi
	_conf=/var/run/jail.${_j}.conf
	#
	# To relieve confusion, show a warning message.
	#
	: ${jail_confwarn:=YES}
	checkyesno jail_confwarn && _confwarn=1
	if [ -r "$jail_conf" -o -r "$_jconf" ]; then
		if ! checkyesno jail_parallel_start; then
			warn "$_conf is created and used for jail $_j."
		fi
	fi
	/usr/bin/install -m 0644 -o root -g wheel /dev/null $_conf || return 1

	eval : \${jail_${_jv}_flags:=${jail_flags}}
	eval _exec=\"\$jail_${_jv}_exec\"
	eval _exec_start=\"\$jail_${_jv}_exec_start\"
	eval _exec_stop=\"\$jail_${_jv}_exec_stop\"
	if [ -n "${_exec}" ]; then
		#   simple/backward-compatible execution
		_exec_start="${_exec}"
		_exec_stop=""
	else
		#   flexible execution
		if [ -z "${_exec_start}" ]; then
			_exec_start="/bin/sh /etc/rc"
			if [ -z "${_exec_stop}" ]; then
				_exec_stop="/bin/sh /etc/rc.shutdown jail"
			fi
		fi
	fi
	eval _interface=\"\${jail_${_jv}_interface:-${jail_interface}}\"
	eval _parameters=\"\${jail_${_jv}_parameters:-${jail_parameters}}\"
	eval _fstab=\"\${jail_${_jv}_fstab:-${jail_fstab:-/etc/fstab.$_j}}\"
	(
		date +"# Generated by rc.d/jail at %Y-%m-%d %H:%M:%S"
		echo "$_j {"
		extract_var $_jv hostname host.hostname - ""
		extract_var $_jv rootdir path - ""
		if [ -n "$_ip" ]; then
			extract_var $_jv interface interface - ""
			jail_handle_ips_option $_ip $_interface
			alias=0
			while : ; do
				eval _x=\"\$jail_${_jv}_ip_multi${alias}\"
				[ -z "$_x" ] && break

				jail_handle_ips_option $_x $_interface
				alias=$(($alias + 1))
			done
			case $need_dad_wait in
			1)
				# Sleep to let DAD complete before
				# starting services.
				echo "	exec.start += \"sleep " \
				$(($(${SYSCTL_N} net.inet6.ip6.dad_count) + 1)) \
				"\";"
			;;
			esac
			# These are applicable only to non-vimage jails. 
			extract_var $_jv fib exec.fib - ""
			extract_var $_jv socket_unixiproute_only \
			    allow.raw_sockets NY YES
		else
			echo "	vnet;"
			extract_var $_jv vnet_interface vnet.interface @ ""
		fi

		echo "	exec.clean;"
		echo "	exec.system_user = \"root\";"
		echo "	exec.jail_user = \"root\";"
		extract_var $_jv exec_prestart exec.prestart 0 ""
		extract_var $_jv exec_poststart exec.poststart 0 ""
		extract_var $_jv exec_prestop exec.prestop 0 ""
		extract_var $_jv exec_poststop exec.poststop 0 ""

		echo "	exec.start += \"$_exec_start\";"
		extract_var $_jv exec_afterstart exec.start 0 ""
		echo "	exec.stop = \"$_exec_stop\";"

		extract_var $_jv consolelog exec.consolelog - \
		    /var/log/jail_${_j}_console.log

		if [ -r $_fstab ]; then
			echo "	mount.fstab = \"$_fstab\";"
		fi

		eval : \${jail_${_jv}_devfs_enable:=${jail_devfs_enable:-NO}}
		if checkyesno jail_${_jv}_devfs_enable; then
			echo "	mount.devfs;"
			eval _ruleset=\${jail_${_jv}_devfs_ruleset:-${jail_devfs_ruleset}}
			case $_ruleset in
			"")	;;
			[0-9]*) echo "	devfs_ruleset = \"$_ruleset\";" ;;
			devfsrules_jail)
				# XXX: This is the default value,
				# Let jail(8) to use the default because
				# mount(8) only accepts an integer. 
				# This should accept a ruleset name.
			;;
			*)	warn "devfs_ruleset must be an integer." ;;
			esac
		fi
		eval : \${jail_${_jv}_fdescfs_enable:=${jail_fdescfs_enable:-NO}}
		if checkyesno jail_${_jv}_fdescfs_enable; then
			echo "	mount.fdescfs;"
		fi
		eval : \${jail_${_jv}_procfs_enable:=${jail_procfs_enable:-NO}}
		if checkyesno jail_${_jv}_procfs_enable; then
			echo "	mount.procfs;"
		fi

		eval : \${jail_${_jv}_mount_enable:=${jail_mount_enable:-NO}}
		if checkyesno jail_${_jv}_mount_enable; then
			echo "	allow.mount;"
		fi

		extract_var $_jv set_hostname_allow allow.set_hostname YN NO
		extract_var $_jv sysvipc_allow allow.sysvipc YN NO
		extract_var $_jv enforce_statfs enforce_statfs - 2
		extract_var $_jv osreldate osreldate
		extract_var $_jv osrelease osrelease

		_zfs_dataset="$(eval echo \$jail_${_jv}_zfs_dataset)"
		if [ -n "$_zfs_dataset" ]; then
			for ds in $_zfs_dataset; do
				echo "	zfs.dataset += ${ds};"
			done
		fi
		for _p in $_parameters; do
			echo "	${_p%\;};"
		done
		echo "}"
	) >> $_conf

	return 0
}

# jail_extract_address argument iface
#	The second argument is the string from one of the _ip
#	or the _multi variables. In case of a comma separated list
#	only one argument must be passed in at a time.
#	The function alters the _type, _iface, _addr and _mask variables.
#
jail_extract_address()
{
	local _i _interface
	_i=$1
	_interface=$2

	if [ -z "${_i}" ]; then
		warn "jail_extract_address: called without input"
		return
	fi

	# Check if we have an interface prefix given and split into
	# iFace and rest.
	case "${_i}" in
	*\|*)	# ifN|.. prefix there
		_iface=${_i%%|*}
		_r=${_i##*|}
		;;
	*)	_iface=""
		_r=${_i}
		;;
	esac

	# In case the IP has no interface given, check if we have a global one.
	_iface=${_iface:-${_interface}}

	# Set address, cut off any prefix/netmask/prefixlen.
	_addr=${_r}
	_addr=${_addr%%[/ ]*}

	# Theoretically we can return here if interface is not set,
	# as we only care about the _mask if we call ifconfig.
	# This is not done because we may want to santize IP addresses
	# based on _type later, and optionally change the type as well.

	# Extract the prefix/netmask/prefixlen part by cutting off the address.
	_mask=${_r}
	_mask=`expr -- "${_mask}" : "${_addr}\(.*\)"`

	# Identify type {inet,inet6}.
	case "${_addr}" in
	*\.*\.*\.*)	_type="inet" ;;
	*:*)		_type="inet6" ;;
	*)		warn "jail_extract_address: type not identified"
			;;
	esac

	# Handle the special /netmask instead of /prefix or
	# "netmask xxx" case for legacy IP.
	# We do NOT support shortend class-full netmasks.
	if [ "${_type}" = "inet" ]; then
		case "${_mask}" in
		/*\.*\.*\.*)	_mask=" netmask ${_mask#/}" ;;
		*)		;;
		esac

		# In case _mask is still not set use /32.
		_mask=${_mask:-/32}

	elif [ "${_type}" = "inet6" ]; then
		# In case _mask is not set for IPv6, use /128.
		_mask=${_mask:-/128}
	fi
}

# jail_handle_ips_option input iface
#	Handle a single argument imput which can be a comma separated
#	list of addresses (theoretically with an option interface and
#	prefix/netmask/prefixlen).
#
jail_handle_ips_option()
{
	local _x _type _i _defif
	_x=$1
	_defif=$2

	if [ -z "${_x}" ]; then
		# No IP given. This can happen for the primary address
		# of each address family.
		return
	fi

	# Loop, in case we find a comma separated list, we need to handle
	# each argument on its own.
	while [ ${#_x} -gt 0 ]; do
		case "${_x}" in
		*,*)	# Extract the first argument and strip it off the list.
			_i=`expr -- "${_x}" : '^\([^,]*\)'`
			_x=`expr -- "${_x}" : "^[^,]*,\(.*\)"`
		;;
		*)	_i=${_x}
			_x=""
		;;
		esac

		_type=""
		_addr=""
		_mask=""
		_iface=""
		jail_extract_address $_i $_defif

		# make sure we got an address.
		case $_addr in
		"")	continue ;;
		*)	;;
		esac

		# Append address to list of addresses for the jail command.
		case $_type in
		inet)
			echo "	ip4.addr += \"${_iface:+${_iface}|}${_addr}${_mask}\";"
		;;
		inet6)
			echo "	ip6.addr += \"${_iface:+${_iface}|}${_addr}${_mask}\";"
			need_dad_wait=1
		;;
		esac
	done
}

jail_config()
{
	local _j _jv

	case $1 in
	_ALL)	return ;;
	esac
	for _j in $@; do
		_j=$(echo $_j | tr /. _)
		_jv=$(echo -n $_j | tr -c '[:alnum:]' _)
		if parse_options $_j $_jv; then 
			echo "$_j: parameters are in $_conf."
		fi
	done
}

jail_console()
{
	local _j _jv _cmd

	# One argument that is not _ALL.
	case $#:$1 in
	0:*|1:_ALL)	err 3 "Specify a jail name." ;;
	1:*)		;;
	esac
	_j=$(echo $1 | tr /. _)
	_jv=$(echo -n $1 | tr -c '[:alnum:]' _)
	shift
	case $# in
	0)	eval _cmd=\${jail_${_jv}_consolecmd:-$jail_consolecmd} ;;
	*)	_cmd=$@ ;;
	esac
	$jail_jexec $_j $_cmd
}

jail_status()
{

	$jail_jls -N
}

jail_start()
{
	local _j _jv _jid _id _name

	if [ $# = 0 ]; then
		return
	fi
	startmsg -n 'Starting jails:'
	case $1 in
	_ALL)
		command=$jail_program
		rc_flags=$jail_flags
		command_args="-f $jail_conf -c"
		if ! checkyesno jail_parallel_start; then
			command_args="$command_args -p1"
		fi
		_tmp=`mktemp -t jail` || exit 3
		if $command $rc_flags $command_args >> $_tmp 2>&1; then
			$jail_jls jid name | while read _id _name; do
				startmsg -n " $_name"
				echo $_id > /var/run/jail_${_name}.id
			done
		else
			cat $_tmp
		fi
		rm -f $_tmp
		startmsg '.'
		return
	;;
	esac
	if checkyesno jail_parallel_start; then
		#
		# Start jails in parallel and then check jail id when
		# jail_parallel_start is YES.
		#
		for _j in $@; do
			_j=$(echo $_j | tr /. _)
			_jv=$(echo -n $_j | tr -c '[:alnum:]' _)
			parse_options $_j $_jv || continue

			eval rc_flags=\${jail_${_jv}_flags:-$jail_flags}
			eval command=\${jail_${_jv}_program:-$jail_program}
			command_args="-i -f $_conf -c $_j"
			(
				_tmp=`mktemp -t jail_${_j}` || exit 3
				if $command $rc_flags $command_args \
				    >> $_tmp 2>&1 </dev/null; then
					startmsg -n " ${_hostname:-${_j}}"
					_jid=$($jail_jls -j $_j jid)
					echo $_jid > /var/run/jail_${_j}.id
				else
					startmsg " cannot start jail " \
					    "\"${_hostname:-${_j}}\": "
					cat $_tmp
				fi
				rm -f $_tmp
			) &
		done
		wait
	else
		#
		# Start jails one-by-one when jail_parallel_start is NO.
		#
		for _j in $@; do
			_j=$(echo $_j | tr /. _)
			_jv=$(echo -n $_j | tr -c '[:alnum:]' _)
			parse_options $_j $_jv || continue

			eval rc_flags=\${jail_${_jv}_flags:-$jail_flags}
			eval command=\${jail_${_jv}_program:-$jail_program}
			command_args="-i -f $_conf -c $_j"
			_tmp=`mktemp -t jail` || exit 3
			if $command $rc_flags $command_args \
			    >> $_tmp 2>&1 </dev/null; then
				startmsg -n " ${_hostname:-${_j}}"
				_jid=$($jail_jls -j $_j jid)
				echo $_jid > /var/run/jail_${_j}.id
			else
				startmsg " cannot start jail " \
				    "\"${_hostname:-${_j}}\": "
				cat $_tmp
			fi
			rm -f $_tmp
		done
	fi
	startmsg '.'
}

jail_stop()
{
	local _j _jv

	if [ $# = 0 ]; then
		return
	fi
	echo -n 'Stopping jails:'
	case $1 in
	_ALL)
		command=$jail_program
		rc_flags=$jail_flags
		command_args="-f $jail_conf -r"
		if checkyesno jail_reverse_stop; then
			$jail_jls name | tail -r
		else
			$jail_jls name
		fi | while read _j; do
			echo -n " $_j"
			_tmp=`mktemp -t jail` || exit 3
			$command $rc_flags $command_args $_j >> $_tmp 2>&1
			if $jail_jls -j $_j > /dev/null 2>&1; then
				cat $_tmp
			else
				rm -f /var/run/jail_${_j}.id
			fi
			rm -f $_tmp
		done
		echo '.'
		return
	;;
	esac
	checkyesno jail_reverse_stop && set -- $(reverse_list $@)
	for _j in $@; do
		_j=$(echo $_j | tr /. _)
		_jv=$(echo -n $_j | tr -c '[:alnum:]' _)
		parse_options $_j $_jv || continue
		if ! $jail_jls -j $_j > /dev/null 2>&1; then
			continue
		fi
		eval command=\${jail_${_jv}_program:-$jail_program}
		echo -n " ${_hostname:-${_j}}"
		_tmp=`mktemp -t jail` || exit 3
		$command -q -f $_conf -r $_j >> $_tmp 2>&1
		if $jail_jls -j $_j > /dev/null 2>&1; then
			cat $_tmp
		else
			rm -f /var/run/jail_${_j}.id
		fi
		rm -f $_tmp
	done
	echo '.'
}

jail_warn()
{

	# To relieve confusion, show a warning message.
	case $_confwarn in
	1)	warn "Per-jail configuration via jail_* variables " \
		    "is obsolete.  Please consider migrating to $jail_conf."
	;;
	esac
}

load_rc_config $name

# doesn't make sense to run in a svcj
jail_svcj="NO"

case $# in
1)	run_rc_command $@ ${jail_list:-_ALL} ;;
*)	jail_reverse_stop="no"
	run_rc_command $@ ;;
esac
